import { Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common'

import Redis from 'ioredis'

import { InjectRedis } from '~/common/decorators/inject-redis.decorator'

import { BizException, BusinessException } from '~/common/exceptions/biz.exception'

import { AppConfig, IAppConfig } from '~/config'
import { genIotAlarmKey, genIotAuthTokenKey, genIotDataKey, genIotDeviceKey } from '~/helper/genRedisKey'
import { DictItemService } from '~/modules/system/dict-item/dict-item.service'
import { generateUUID, md5 } from '~/utils'
import {
  AlarmRuleDto,
  DeviceAttrDto,
  DeviceAuthDto,
  DeviceConfigDto,
  DeviceQueryDto,
  DeviceSignDto,
  DeviceUpdateDto,
} from '../device/device.dto'
import { DeviceEntity } from '../device/device.entity'
import { DeviceToken } from '../device/device.model'
import { DeviceService } from '../device/device.service'
import { UserAlarmDto, UserAlarmUpdateDto } from '../user/userAlarm.dto'
import { UserAlarmEntity } from '../user/userAlarm.entity'
import { UserAlarmService } from '../user/userAlarm.service'
import { UserDataDto } from '../user/userData.dto'
import { UserDataEntity } from '../user/userData.entity'
import { UserDataService } from '../user/userData.service'
import { UserSosDto, UserSosQueryDto, UserSosUpdateDto } from '../user/userSos.dto'
import { UserSosEntity } from '../user/userSos.entity'
import { UserSosService } from '../user/userSos.service'
import { IotEventDto } from './iot.dto'

// const PRIVATE_SECRET_KEY = '1234567890123456'
// const BASE_URL = 'http://192.168.0.104:7001/api/health/'
// const SECRET_KEY = md5(BASE_URL + PRIVATE_SECRET_KEY)
// const SECRET_KEY = '7fd4b5dc98af93b740ee2ab3345cc965'

const IOT_ALARM_LOCK = '__iot_alarm_lock__'

@Injectable()
export class IotService {
  async checkDeviceUser(token: string, device: DeviceEntity) {
    if (!device.userId) {
      // 重新查询设备信息
      device = await this.deviceService.findOne({ uid: device.uid } as DeviceQueryDto)
      if (!device.userId) {
        throw new BizException('设备未绑定用户')
      }
      this.redis.set(genIotAuthTokenKey(token), JSON.stringify(device), 'EX', 60 * 60)
    }
  }

  private logger = new Logger(IotService.name)

  constructor(
    private deviceService: DeviceService,
    private userDataService: UserDataService,
    private userAlarmService: UserAlarmService,
    private userSosService: UserSosService,
    private dictItemService: DictItemService,
    @InjectRedis() private readonly redis: Redis,
    @Inject(AppConfig.KEY) private appConfig: IAppConfig,
  ) {
  }

  /**
   * 设备认证
   * @param authDto
   * @param signDto
   */
  async auth(authDto: DeviceAuthDto, signDto: DeviceSignDto): Promise<DeviceToken> {
    const body = JSON.stringify(authDto)
    const sign = md5(body + signDto.ts + this.appConfig.iotSecretKey)
    if (sign !== signDto.sign) {
      throw new BusinessException('401:签名无效')
    }
    const device = await this.deviceService.findOne({ sn: authDto.sn, mac: authDto.mac } as DeviceQueryDto)
    if (device == null) {
      throw new BusinessException('401:认证失败')
    }
    const token = generateUUID(32)
    const cacheToken = new DeviceToken(device.uid, token)
    await this.redis.set(genIotAuthTokenKey(token), device.uid, 'EX', 60 * 60 * 24)
    return cacheToken
  }

  async validateToken(token: string, uid: string): Promise<DeviceEntity> {
    const _uid = await this.redis.get(genIotAuthTokenKey(token))
    if (_uid !== uid) {
      this.redis.del(token)
      throw new UnauthorizedException('401:Token无效')
    }
    const deviceJson = await this.redis.get(genIotDeviceKey(uid))
    let device: DeviceEntity
    if (deviceJson) {
      device = JSON.parse(deviceJson) as DeviceEntity
    }
    else {
      device = await this.deviceService.findOne({ uid } as DeviceQueryDto)
      if (device) {
        this.redis.set(genIotDeviceKey(uid), JSON.stringify(device))
      }
    }
    return device
  }

  async saveAttr(uid: string, data: DeviceAttrDto): Promise<void> {
    data.uid = uid
    data.updatedAt = new Date()
    return this.deviceService.saveAttr(data)
  }

  async getAlarmRule(type: number): Promise<AlarmRuleDto[]> {
    const cacheKey = genIotAlarmKey(`all_rule`)
    const cacheStr = await this.redis.get(cacheKey)
    let alarmList: AlarmRuleDto[] = null
    if (cacheStr) {
      alarmList = JSON.parse(cacheStr) as AlarmRuleDto[]
    }
    else {
      const dictList = await this.dictItemService.findByType({ code: 'device_alarm_rule', status: 1 })
      alarmList = dictList.map((item) => {
        const [min, max] = JSON.parse(item.value) as [number, number]
        return {
          id: item.orderNo,
          type: Number.parseInt(item.remark) || 0,
          status: item.status,
          min,
          max,
        } as AlarmRuleDto
      })
      await this.redis.set(cacheKey, JSON.stringify(alarmList))
    }

    return alarmList.filter(item => item.type === type)
  }

  async checkAlarm(item: UserDataDto, cacheObj: UserDataDto, device: DeviceEntity) {
    let value = item.data?.value
    if (!value) {
      return
    }
    value = Number.parseInt(value)
    this.logger.error(value)

    const alarmRuleList = await this.getAlarmRule(item.type)
    if (!(alarmRuleList && alarmRuleList.length > 0)) {
      return
    }
    // this.logger.log(`item = `, item)

    for (const rule of alarmRuleList) {
      const cacheKey = genIotAlarmKey(`${device.userId}:${rule.id}`)
      const alarmId = await this.redis.get(cacheKey)
      const inAlarmRange = value >= rule.min && value <= rule.max
      const isLocked = alarmId === IOT_ALARM_LOCK
      // 当前有告警
      if (inAlarmRange) {
        // 之前无告警
        if (!alarmId) {
          const lockResult = await this.redis.set(cacheKey, IOT_ALARM_LOCK, 'NX', 'GET')
          if (!lockResult) {
            continue
          }
          const data = {
            userId: device.userId,
            uid: device.uid,
            type: rule.id,
            content: `告警值: ${value}, 告警范围：[${rule.min},${rule.max}]`,
            status: 0,
          } as UserAlarmEntity
          try {
            const alarmEntity = await this.userAlarmService.saveAlarm(data)
            await this.redis.set(cacheKey, alarmEntity.id)
          }
          catch (error) {
            await this.redis.del(cacheKey)
            throw error
          }
        }
      }
      // 当前无告警
      else {
        // 之前有告警
        if (alarmId && !isLocked) {
          // 结束告警
          await this.userAlarmService.updateAlarm(Number.parseInt(alarmId), { status: 1, endAt: new Date() } as UserAlarmUpdateDto)
          await this.redis.del(cacheKey)
        }
      }
    }
  }

  async saveData(device: DeviceEntity, dataList: UserDataDto[]): Promise<void> {
    const tempList: UserDataDto[] = []

    for (const item of dataList) {
      const cacheKey = genIotDataKey(`${device.uid}:${item.type}`)
      const cacheStr = await this.redis.get(cacheKey)

      let cacheObj: UserDataDto = null
      if (cacheStr) {
        cacheObj = JSON.parse(cacheStr) as UserDataDto
        if (cacheObj.ts >= item.ts) {
          continue
        }
      }
      tempList.push(item)
      try {
        await this.checkAlarm(item, cacheObj, device)
      }
      catch (err) {
        this.logger.error(err)
      }
      await this.redis.set(cacheKey, JSON.stringify(item))
    }
    try {
      const dataList = tempList.map(item => ({
        uid: device.uid,
        type: item.type,
        ts: new Date(item.ts * 1000),
        data: item.data,
        userId: device.userId,
      } as UserDataEntity))
      await this.userDataService.saveData(dataList)
    }
    catch (error) {
      this.logger.error(error)
    }
  }

  async saveAlarm(device: UserAlarmEntity, dataList: UserAlarmDto[]): Promise<void> {
    for (const item of dataList) {
      const cacheKey = genIotDataKey(`${device.uid}:${item.type}`)
      const ts = await this.redis.get(cacheKey)
      if (!(item.startAt.getTime() > Number.parseInt(ts || '0'))) {
        continue
      }
      const data = {
        uid: device.uid,
        type: item.type,
        startAt: item.startAt,
        status: 0,
        userId: device.userId,
      } as UserAlarmEntity

      try {
        await this.userAlarmService.saveAlarm(data)
      }
      catch (error) {
        this.logger.error(error)
      }
      await this.redis.set(cacheKey, item.startAt.getTime())
    }
  }

  async saveSos(device: DeviceEntity, dataList: UserSosDto[]): Promise<void> {
    for (const item of dataList) {
      const cacheKey = genIotDataKey(`${device.userId}:${item.startAt}`)
      const startAt = await this.redis.get(cacheKey)
      if (!(item.startAt.getTime() > Number.parseInt(startAt || '0'))) {
        continue
      }
      const data = {
        userId: device.userId,
        uid: device.uid,
        startAt: item.startAt,
        status: 0,
      } as UserSosEntity

      try {
        await this.userSosService.saveSos(data)
      }
      catch (error) {
        this.logger.error(error)
      }
      await this.redis.set(cacheKey, item.startAt.getTime())
    }
  }

  async getAttr(uid: string): Promise<DeviceConfigDto[]> {
    const dictList = await this.dictItemService.findByType({ code: 'device_data_collect' })
    return dictList.map(item => (({
      type: item.orderNo,
      value: Number.parseInt(item.value),
      status: item.status,
    }) as DeviceConfigDto))
  }

  /**
   * 处理设备事件
   * @param device
   * @param event
   */
  async handleEvent(device: DeviceEntity, event: IotEventDto) {
    if (event.event === 'heart') {
      const data = {} as DeviceUpdateDto
      await this.deviceService.update(device.id, data)
    }
    else if (event.event === 'sos') {
      const queryDto = {
        userId: device.userId,
        uid: device.uid,
        status: 0,
      } as UserSosQueryDto
      // 查询未处理呼救记录
      const savedData = await this.userSosService.findOne(queryDto)
      if (savedData == null) {
        const data = {
          userId: device.userId,
          uid: device.uid,
          startAt: new Date(),
          times: 1,
          status: 0,
        } as UserSosEntity
        await this.userSosService.saveSos(data)
      }
      else {
        const data = {
          times: savedData.times + 1,
        } as UserSosUpdateDto
        await this.userSosService.updateSos(savedData.id, data)
      }
    }
  }
}
